/*
 *  Copyright 2010 Farent-Solutions Ltd.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package com.farent.solutions.plugins.aribaweb.awl.completion;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.AbstractDocument;
import javax.swing.text.JTextComponent;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.xml.lexer.XMLTokenId;
import org.netbeans.editor.BaseDocument;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.windows.TopComponent;
import org.netbeans.modules.xml.axi.AXIComponent;
import org.netbeans.modules.xml.axi.AXIModel;
import org.netbeans.modules.xml.axi.AXIModelFactory;
import org.netbeans.modules.xml.axi.AbstractAttribute;
import org.netbeans.modules.xml.axi.AbstractElement;
import org.netbeans.modules.xml.axi.AnyAttribute;
import org.netbeans.modules.xml.axi.AnyElement;
import org.netbeans.modules.xml.axi.Attribute;
import org.netbeans.modules.xml.axi.Element;
import org.netbeans.modules.xml.schema.completion.spi.CompletionModelProvider.CompletionModel;

/**
 *
 * @author user
 */
public class CompletionUtil {

    public static final String TAG_FIRST_CHAR = "<", //NOI18N
            TAG_LAST_CHAR = ">", //NOI18N
            END_TAG_PREFIX = "</", //NOI18N
            END_TAG_SUFFIX = "/>"; //NOI18N
    private static final Logger log = Logger.getLogger(CompletionUtil.class.getName());
    // Pattern: ("<" + ("blank space" or "\n") + "any characters")
    //          ("<" + "/" + ("blank space" or "\n") + "any characters")
    public static final Pattern PATTERN_TEXT_TAG_EOLs = Pattern.compile("</?[\\s]+.*");

    public static AbstractCompletionItem1 getEndTagCompletionItem(JTextComponent component,
            BaseDocument document) {
        int caretPos = component.getCaret().getDot();
        try {
            ((AbstractDocument) document).readLock();

            TokenHierarchy tokenHierarchy = TokenHierarchy.get(document);
            TokenSequence tokenSequence = tokenHierarchy.tokenSequence();
//            String tokenSe = tokenSequence

            String incompleteTagName = findIncompleteTagName(caretPos, tokenSequence);
            if (isCaretInsideTag(caretPos, tokenSequence)) {
                return null;
            }

            boolean beforeUnclosedStartTagFound = isUnclosedStartTagFoundBefore(
                    caretPos, tokenSequence);
            if (!beforeUnclosedStartTagFound) {
                return null;
            }

            Token token = tokenSequence.token();
            String startTagName = getTokenTagName(token);
            if (startTagName == null) {
                return null;
            }

            boolean closingTagFound = isClosingEndTagFoundAfter(caretPos,
                    tokenSequence, startTagName);
            if (closingTagFound) {
                return null;
            }

            AbstractCompletionItem1 resultItem = null;
//            if ((incompleteTagName != null)
//                    && (!startTagName.startsWith(incompleteTagName))) {
//                resultItem = new TagLastCharResultItem(incompleteTagName, tokenSequence);
//            } else {
//                resultItem = new EndTagResultItem(startTagName, tokenSequence);
//            }
            return resultItem;
        } catch (Exception e) {
            log.log(Level.WARNING,
                    e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e);
            return null;
        } finally {
            ((AbstractDocument) document).readUnlock();
        }
    }

    private static boolean isUnclosedStartTagFoundBefore(int caretPos,
            TokenSequence tokenSequence) {
        tokenSequence.move(caretPos);

        boolean startTagFound = false, tagLastCharFound = false;
        Stack<String> existingEndTags = new Stack<String>();
        String startTagName, endTagName;

        while (tokenSequence.movePrevious()) {
            Token token = tokenSequence.token();
            if (isTagLastChar(token)) {
                tagLastCharFound = true;
            } else if (isEndTagPrefix(token)) {
                tagLastCharFound = startTagFound = false;
                endTagName = getTokenTagName(token);
                if (endTagName != null) {
                    existingEndTags.push(endTagName);
                }
            } else if (isTagFirstChar(token) && tagLastCharFound) {
                startTagName = getTokenTagName(token);
                endTagName = existingEndTags.isEmpty() ? null : existingEndTags.peek();
                if ((startTagName != null) && (endTagName != null)
                        && startTagName.equals(endTagName)) {
                    existingEndTags.pop();
                    tagLastCharFound = startTagFound = false;
                    continue;
                }
                startTagFound = true;
                break;
            }
        }
        return startTagFound;
    }

    private static String findIncompleteTagName(int caretPos, TokenSequence tokenSequence) {
        if (!isTokenSequenceUsable(tokenSequence)) {
            return null;
        }

        boolean tagFirstCharFound = false;
        Token token = null;
        String incompleteTagName = null;

        tokenSequence.move(caretPos);
        tokenSequence.moveNext();
        do {
            token = tokenSequence.token();
            TokenId tokenID = token.id();
            if (tokenID.equals(XMLTokenId.TAG)) {
                String tokenText = token.text().toString();
                if ((tokenText == null) || tokenText.isEmpty()) {
                    continue;
                }

                if (tokenText.startsWith(TAG_FIRST_CHAR)) {
                    if (tokenSequence.offset() < caretPos) {
                        tagFirstCharFound = true;
                        incompleteTagName = getTokenTagName(token);
                        break;
                    }
                } else {
                    return null;
                }
            }
        } while (tokenSequence.movePrevious());

        if (!tagFirstCharFound) {
            return null;
        }

        tokenSequence.move(caretPos);
        while (tokenSequence.moveNext()) {
            token = tokenSequence.token();
            TokenId tokenID = token.id();
            if (tokenID.equals(XMLTokenId.TAG)) {
                String tokenText = token.text().toString();
                if ((tokenText == null) || tokenText.isEmpty()) {
                    continue;
                }

                if (tokenText.contains(TAG_LAST_CHAR)) {
                    return null;
                } else {
                    return incompleteTagName;
                }
            }
        }
        return incompleteTagName;
    }

    private static boolean isClosingEndTagFoundAfter(int caretPos,
            TokenSequence tokenSequence, String startTagName) {
        if ((tokenSequence == null) || (startTagName == null)) {
            return false;
        }
        tokenSequence.move(caretPos);

        int unclosedTagCount = 1;
        while (tokenSequence.moveNext()) {
            Token token = tokenSequence.token();
            String nextTagName = getTokenTagName(token);
            if (nextTagName != null) {
                if (startTagName.equals(nextTagName)) {
                    // fix for issue #185048
                    // (http://netbeans.org/bugzilla/show_bug.cgi?id=185048)
                    unclosedTagCount += isEndTagPrefix(token) ? -1
                            : (isTagFirstChar(token) ? 1 : 0);
                }
            } else if (isTagLastChar(token) && (unclosedTagCount < 1)) {
                return true;
            }
        }
        return false;
    }

    public static boolean isTokenSequenceUsable(TokenSequence tokenSequence) {
        return ((tokenSequence != null) && (tokenSequence.isValid())
                && (!tokenSequence.isEmpty()));
    }

    public static boolean isCaretInsideTag(int caretPos, TokenSequence tokenSequence) {
        if (!isTokenSequenceUsable(tokenSequence)) {
            return false;
        }

        boolean tagFirstCharFound = false, tagLastCharFound = false;
        Token token = null;

        tokenSequence.move(caretPos);
        tokenSequence.moveNext();
        do {
            token = tokenSequence.token();
            if (isTagFirstChar(token) || isEndTagPrefix(token)) {
                tagFirstCharFound = true;
                break;
            }
        } while (tokenSequence.movePrevious());

        if (!tagFirstCharFound) {
            return false;
        }

        while (tokenSequence.moveNext()) {
            token = tokenSequence.token();
            int tokenOffset = tokenSequence.offset();
            boolean isEndTagSuffix = isEndTagSuffix(token);
            if (isTagLastChar(token) || isEndTagSuffix) {
                if ((tokenOffset >= caretPos)
                        || (isEndTagSuffix && (tokenOffset == caretPos - 1))) {
                    tagLastCharFound = true;
                }
                break;
            }
        }
        return (tagFirstCharFound && tagLastCharFound);
    }

    public static boolean isTagFirstChar(Token token) {
        if (token == null) {
            return false;
        }

        TokenId tokenID = token.id();
        if (tokenID.equals(XMLTokenId.TAG)) {
            String tokenText = token.text().toString();
            if ((!isEndTagPrefix(token)) && tokenText.startsWith(TAG_FIRST_CHAR)) {
                return true;
            }
        }
        return false;
    }

    public static boolean isTagLastChar(Token token) {
        if (token == null) {
            return false;
        }

        TokenId tokenID = token.id();
        if (tokenID.equals(XMLTokenId.TAG)) {
            String tokenText = token.text().toString();
            if (tokenText.equals(TAG_LAST_CHAR)) {
                return true;
            }
        }
        return false;
    }

    public static boolean isEndTagPrefix(Token token) {
        if (token == null) {
            return false;
        }

        TokenId tokenID = token.id();
        if (tokenID.equals(XMLTokenId.TAG)) {
            String tokenText = token.text().toString();
            if (tokenText.startsWith(END_TAG_PREFIX)) {
                return true;
            }
        }
        return false;
    }

    public static boolean isEndTagSuffix(Token token) {
        if (token == null) {
            return false;
        }

        TokenId tokenID = token.id();
        if (tokenID.equals(XMLTokenId.TAG)) {
            String tokenText = token.text().toString();
            if (tokenText.equals(END_TAG_SUFFIX)) {
                return true;
            }
        }
        return false;
    }

    public static boolean isTextTag(Token token) {
        if (token == null) {
            return false;
        }

        TokenId tokenID = token.id();
        if (!tokenID.equals(XMLTokenId.TEXT)) {
            return false;
        }

        String tokenText = token.text().toString();
        boolean result = PATTERN_TEXT_TAG_EOLs.matcher(tokenText).matches();
        return result;
    }

    public static String getTokenTagName(Token token) {
        if (token == null) {
            return null;
        }

        int index = -1;
        if (isTagFirstChar(token)) {
            index = TAG_FIRST_CHAR.length();
        } else if (isEndTagPrefix(token)) {
            index = END_TAG_PREFIX.length();
        } else {
            return null;
        }
        String tokenText = token.text().toString(),
                tagName = (tokenText == null ? null : tokenText.substring(index));
        return tagName;
    }

    /**
     * Can't provide completion for XML documents based on DTD
     * and for those who do not declare namespaces in root tag.
     */
    public static boolean canProvideCompletion(JTextComponent component, BaseDocument doc) {
        FileObject file = getPrimaryFile();
        if (file == null) {
            return false;
        }

        //for .awl documents
        if (!"awl".equals(file.getExt())) { //NOI18N
            return true;
        }

        //Only supporting prefixed tags at the moment
//        String firstTag = getStartTag(component, doc);
//        String prefix = getPrefixFromTag(firstTag);
//        if (prefix == null || !AwlCompletionContext.isPrefixSupported(prefix)) {
//            return false;
//        }
        return true;
    }

    /**
     * For debugging purposes only.
     */
    public static void printPath(List<QName> path) {
        StringBuffer buffer = new StringBuffer();
        for (QName item : path) {
            if (buffer.toString().equals("")) {
                buffer.append(item);
            } else {
                buffer.append("/" + item);
            }
        }
        //System.out.println(buffer);
    }

    public static FileObject getPrimaryFile() {
        TopComponent activatedTC = TopComponent.getRegistry().getActivated();
        if (activatedTC == null) {
            return null;
        }
        DataObject activeFile = activatedTC.getLookup().lookup(DataObject.class);
        if (activeFile == null) {
            return null;
        }

        return activeFile.getPrimaryFile();
    }

    /**
     * Returns the prefix from the element's tag.
     */
    public static String getPrefixFromTag(String tagName) {
        if (tagName == null) {
            return null;
        }
        int index = tagName.indexOf(":");
        if (index == -1) {
            return null;
        }

        String prefixName = tagName.substring(0, index);
        if (prefixName.startsWith(END_TAG_PREFIX)) {
            prefixName = prefixName.substring(END_TAG_PREFIX.length());
        } else if (prefixName.startsWith(TAG_FIRST_CHAR)) {
            prefixName = prefixName.substring(TAG_FIRST_CHAR.length());
        }
        return prefixName;
    }

    /**
     * Returns the local name from the element's tag.
     */
    public static String getLocalNameFromTag(String tagName) {
        if (tagName == null) {
            return null;
        }
        return (tagName.indexOf(":") == -1) ? tagName
                : tagName.substring(tagName.indexOf(":") + 1, tagName.length());
    }

    public static String getStartTag(JTextComponent component, BaseDocument document) {
        int caretPos = component.getCaret().getDot();
        Token token;
        String incompleteTagName = "";
        try {
            ((AbstractDocument) document).readLock();
            TokenHierarchy tokenHierarchy = TokenHierarchy.get(document);
            TokenSequence tokenSequence = tokenHierarchy.tokenSequence();

            tokenSequence.move(caretPos);
            tokenSequence.moveNext();
            do {
                token = tokenSequence.token();
                TokenId tokenID = token.id();
                if (tokenID.equals(XMLTokenId.TAG)) {
                    String tokenText = token.text().toString();
                    if ((tokenText == null) || tokenText.isEmpty()) {
                        continue;
                    }

                    if (tokenText.startsWith(TAG_FIRST_CHAR)) {
                        if (tokenSequence.offset() < caretPos) {
                            incompleteTagName = getTokenTagName(token);
                            break;
                        }
                    } else {
                        return "";
                    }
                }
            } while (tokenSequence.movePrevious());

        } catch (Exception ex) {
        } finally {
            ((AbstractDocument) document).readUnlock();
        }
        return incompleteTagName;
    }

    /**
     * Returns the list of child-elements for a given element.
     *
     * AribaWeb schema allows any in all components. hence no filtering required
     */
    public static List<AbstractCompletionItem1> getElements( AwlCompletionContext context) {

        Element element = findAXIElementAtContext(context);

        List<AbstractCompletionItem1> results = new ArrayList<AbstractCompletionItem1>();
        if (element != null) {
            for (AbstractElement ae : element.getChildElements()) {
                AXIComponent original = ae.getOriginal();
                if (original.getTargetNamespace() == null) {  //no namespace
                    continue;
                }
                if (original instanceof AnyElement) {
                    results.addAll(substituteAny((AnyElement) original, context));
                    continue;
                }
            }
        }
        else 
        {
            for (CompletionModel cm : context.getCompletionModels()) {
                if (cm == null) continue;
                AXIModel am = AXIModelFactory.getDefault().getModel(cm.getSchemaModel());
                for(Element e : am.getRoot().getElements()) {
                    addCompletionItems(e, context, cm.getSuggestedPrefix(), results );
                }
            }
        }
        return results;
    }

    /**
     * Substitue any or anyAttribute with a valid list of items.
     */
    private static List<AbstractCompletionItem1> substituteAny(AXIComponent any,
            AwlCompletionContext context) {
        List<AbstractCompletionItem1> items = new ArrayList<AbstractCompletionItem1>();
        for (CompletionModel cm : context.getCompletionModels()) {
            if (cm == null) continue;
            else populateItemsForAny(cm, any, context, items);
        }

        return items;
    }

    private static void populateItemsForAny(CompletionModel cm, AXIComponent any,
            AwlCompletionContext context, List<AbstractCompletionItem1> items) {
        if (cm == null) return;

        AXIModel am = AXIModelFactory.getDefault().getModel(cm.getSchemaModel());
        if (any instanceof AnyElement) {
            for(Element e : am.getRoot().getElements()) {
                addCompletionItems(e, context, cm.getSuggestedPrefix(), items);
            }
        }
        if (any instanceof AnyAttribute) {
            for(Attribute a : am.getRoot().getAttributes()) {
                addCompletionItems(a, context, cm.getSuggestedPrefix(), items);
            }
        }
        StringBuilder out = new StringBuilder();
        for (AbstractCompletionItem1 o : items) {
            out.append(o.getReplacementText());
            out.append("\t");
        }
        System.out.println(out.toString());
    }


    private static void addCompletionItems(AXIComponent axi,
            AwlCompletionContext context, String prefix, List<AbstractCompletionItem1> results) {
        String typedChars = context.getTypedChars();
        AbstractCompletionItem1 item = null;
        item = createResultItem(axi, typedChars, prefix, context);
        if (item == null) {
            return;
        }
        if (typedChars == null) {
            results.add(item);
        } else if (isResultItemTextStartsWith(item, typedChars)) {
            results.add(item);
        }
        
    }

    private static boolean isResultItemTextStartsWith(AbstractCompletionItem1 resultItem,
            String text) {
        if ((resultItem == null) || (text == null)) {
            return false;
        }

        String resultText = resultItem.getReplacementText();
        int startIndex = 0;
        if (resultText.startsWith(END_TAG_PREFIX) && (!text.startsWith(END_TAG_PREFIX))) {
            startIndex = END_TAG_PREFIX.length();
        } else if (resultText.startsWith(TAG_FIRST_CHAR)
                && (!text.startsWith(TAG_FIRST_CHAR))) {
            startIndex = TAG_FIRST_CHAR.length();
        }
        boolean result = resultText.startsWith(text, startIndex);
        return result;
    }

    /**
     * Returns the appropriate AXIOM element for a given context.
     */
    public static Element findAXIElementAtContext(
            AwlCompletionContext context) {
        List<QName> path = context.getPathFromRoot();
        if (path == null || path.isEmpty()) {
            return null;
        }

        CompletionModel cm = null;
        QName tag = context.getPathFromRoot().get(0);
        String tns = tag.getNamespaceURI();
        if (tns != null && !tns.isEmpty()) {
            cm = context.getCompletionModelMap().get(tns);
        }
        if (cm == null) {
            return null;
        }

        AXIModel am = AXIModelFactory.getDefault().getModel(cm.getSchemaModel());
        AXIComponent parent = am.getRoot();
        if (parent == null) {
            return null;
        }

        AXIComponent child = null;
        for (QName qname : path) {
            child = findChildElement(parent, qname);
            parent = child;
        }

        if (child != null && (child instanceof Element)) {
            return (Element) child;
        }

        return null;
    }

    private static AXIComponent findChildElement(AXIComponent parent,
            QName qname) {
        if (parent == null) {
            return null;
        }
        for (AbstractElement element : parent.getChildElements()) {
            if (!(element instanceof Element)) {
                continue;
            }
            Element e = (Element) element;
            if (qname.getLocalPart().equals(e.getName())) {
                return element;
            }
        }
        return null;
    }

    private static AbstractCompletionItem1 createResultItem(AXIComponent axi, String typedChars,
            String prefix, AwlCompletionContext context) {
        AbstractCompletionItem1 item = null;
        if (axi instanceof AbstractElement) {
            if (prefix == null) {
                item = new ElementResultItem((AbstractElement) axi, context);
            } else {
                item = new ElementResultItem((AbstractElement) axi, typedChars, prefix, context);
            }
        }

//        if(axi instanceof AbstractAttribute) {
//            Attribute a = (Attribute)axi;
//            if(prefix == null) {
//                if(!context.getExistingAttributes().contains(a.getName()))
//                    item = new AttributeResultItem((AbstractAttribute)axi, context);
//            } else {
//                if(!context.getExistingAttributes().contains(prefix+":"+a.getName()))
//                    item = new AttributeResultItem((AbstractAttribute)axi, prefix, context);
//            }
//        }

        return item;
    }
}
